package eval.cm.xml.parser.simple;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import eval.cm.util.CollectionUtils;

/**
 * Encapsulates a xml tag element in our simple xml parser.
 * It can have either text or children
 * 
 * @author constantin
 *
 */
public class Element {
	
	/**
	 * The xml tag name(e.g. 'person' in &lt;person/&gt;)
	 */
	private final String name;
	
	/**
	 * A list of child elements.
	 * This is null by default to avoid unnecessary instantiation of an empty list
	 * when the element has only text.
	 * <p>
	 * I chose not to default initialize this for memory avoidance
	 */
	private List<Element> children;
	
	/**
	 * For querying and browsing purposes we maintain a map of children
	 * tag names
	 */
	private Map<String,List<Element>> childrenByTagName;
	
	/**
	 * The value/text of the element.
	 * This is null when this element has at least one child element
	 */
	private String value;
	
	/**
	 * This keeps a reference to the parent element
	 */
	private Element parentElement;
	
	public Element( final String tagName ) {
		if( null == tagName ) {
			throw new NullPointerException("Tag name cannot be null");
		}
		this.name = tagName;
	}
	
	/**
	 * Queries elements relative to this element.<br>
	 * @param xpath A string representing the path of element names
	 * relative to this element (e.g. 'address/line_1').
	 * <p>
	 * Whildcard '*' is supported only one level (e.g root/_*_/address).
	 * <p>
	 * /text() is not supported as the text is not treated as a node.
	 * @return A list of elements meeting the xpath criteria.
	 * If none of the elements meet the criteria the list is empty ( not a null).
	 */
	public List<Element> queryElements( final String xpath ) {
		final String[] tokens = xpath.split("/");
		final List<Element> result = new ArrayList<Element>();
		queryElements(result, Arrays.asList(this), tokens, 0);
		return result;
	}
	
	/**
	 * Implementing a recursive collection of elements
	 * meeting the xpath criteria
	 * @param collector This is a collector list where all the leafs are gathered
	 * @param elements The elements of the current level
	 * @param xPathToks The xpath token array
	 * @param xPathIndex The current index in the token array
	 */
	protected void queryElements(
		final List<Element> collector,
		final List<Element> elements,
		final String[] xPathToks,
		final int xPathIndex
	) {
		if( null == elements) {
			return;
		}
		if( xPathIndex == xPathToks.length - 1 ) {
			//this is the condition to end recursion
			for(Element ele: elements) {
				List<Element> subs;
				if("*".equals(xPathToks[xPathIndex])) {
					subs = ele.getChildren();
				} else {
					subs = ele.getChildrenByName(xPathToks[xPathIndex]);
				}
				if( null != subs ) {
					//up here all the elements in subs collection should meet the path
					collector.addAll(subs);
				}
			}
			return;
		}
		//here we are on an intermediate level...
		for(Element ele: elements) {
			List<Element> subs;
			if("*".equals(xPathToks[xPathIndex])) {
				subs = ele.getChildren();
			} else {
				subs = ele.getChildrenByName(xPathToks[xPathIndex]);
			}
			if( null != subs ) {
				//...where we apply recursion
				queryElements(collector, subs, xPathToks, xPathIndex+1);
			}
		}
	}

	public String getName() {
		return name;
	}
	
	/**
	 * Registers a child element to this element.
	 * @param child The child element
	 */
	public void appendChild(final Element child) {
		if( null == this.children ) {
			this.children = new ArrayList<Element>();
			this.childrenByTagName = new HashMap<String,List<Element>>();
		}
		this.children.add(child);
		CollectionUtils.addItemToMapOfLists(
				this.childrenByTagName, child.getName(), child);
		child.setParentElement(this);
	}

	public List<Element> getChildren() {
		return children;
	}

	public List<Element> getChildrenByName(final String tagName) {
		if( null == this.childrenByTagName ) {
			return null;
		}
		return this.childrenByTagName.get(tagName);
	}

	public void setParentElement(final Element parentElement) {
		this.parentElement = parentElement;
	}

	public Element getParentElement() {
		return parentElement;
	}

	/**
	 * Sets the text value of this element
	 * @param value A string representing the text value of this element
	 */
	public void setValue(final String value) {
		this.value = value;
	}

	/**
	 * Returns the text value of this element
	 * @return A string representing the text value of this element
	 */
	public String getValue() {
		return value;
	}
	
	/**
	 * Method that outputs this element back into xml format
	 * @return XML format representation of this element
	 */
	public String toXML() {
		final StringBuilder sbuff = new StringBuilder();
		sbuff.append('<').append(this.name);
		if( null == this.children && null == this.value ) {
			sbuff.append("/>");
			return sbuff.toString();
		}
		sbuff.append('>');
		if( null != this.children ) {
			for(Element kid: this.children) {
				sbuff.append(kid.toXML());
			}
		} else {
			sbuff.append(this.value);
		}
		sbuff.append("</").append(this.name).append('>');
		return sbuff.toString();
	}

}
